# -*- coding: utf-8 -*-

from enum import Enum
from typing import List
from typing import Dict
from typing import Set

from conf.entity.module.grpc_config import GrpcConfig, GrpcMappingConfig


class IceGrpcMethodMap:
    ice_method: str
    grpc_method: str

    def __init__(self, ice_method: str, grpc_method: str):
        self.ice_method = ice_method
        self.grpc_method = grpc_method


class IceGrpcMethodGroup:
    ice_servant: str
    grpc_servant: str
    methods: List[IceGrpcMethodMap]

    def __init__(self, ice_servant: str, grpc_servant: str):
        self.ice_servant = ice_servant
        self.grpc_servant = grpc_servant
        self.methods = []

    def add_method(self, method: IceGrpcMethodMap):
        self.methods.append(method)


class GrpcStructMapping(object):
    grpc_2_java_dict: Dict[str, object]
    struct_dict: Dict[str, object]
    map_dict: Dict[str, object]
    existed_struct_set: Set[str]
    basic_proto_type_set: Set[str]

    def __init__(self):
        self.grpc_2_java_dict = {}
        self.struct_dict = {}
        self.map_dict = {}
        self.existed_struct_set = set()
        self.basic_proto_type_set = set()
        self.basic_proto_type_set.add('string')
        self.basic_proto_type_set.add('bool')
        self.basic_proto_type_set.add('int32')
        self.basic_proto_type_set.add('int64')


class IceStructMapping(object):
    ice_2_grpc_dict: Dict[str, object]
    sequence_dict: Dict[str, str]
    sequence_unresolved_dict: Dict[str, str]
    dictionary_dict: Dict[str, List[str]]
    basic_ice_2_proto_type_dict = {
        'string': 'string',
        'bool': 'bool',
        'int': 'int32',
        'long': 'int64',
        'float': 'float',
        'double': 'double'
    }
    ice_2_proto_type_dict = dict(basic_ice_2_proto_type_dict)
    ice_2_grpc_services: List[object]
    ice_2_grpc_method_groups: List[IceGrpcMethodGroup]

    def __init__(self):
        self.ice_2_grpc_dict = {}
        self.sequence_dict = {}
        self.sequence_unresolved_dict = {}
        self.dictionary_dict = {}
        self.ice_2_grpc_services = []
        self.ice_2_grpc_method_groups = []


class JavaStructMapping(object):
    java_2_grpc_dict: Dict[str, object]
    java_2_grpc_services: List[object]

    def __init__(self):
        self.java_2_grpc_dict = {}
        self.java_2_grpc_services = []


class GrpcFullMapping(object):
    """
    All the needed mappings for gRPC process

    :filter_mapping: use filter mapping to skip previous definitions
    :grpc_mapping: gRPC to ice/Java mapping
    :ice_mapping: ice to gRPC mapping
    :java_mapping: Java to gRPC mapping
    """
    filter_mapping: GrpcMappingConfig
    grpc: GrpcStructMapping
    ice: IceStructMapping
    java: JavaStructMapping

    def __init__(self):
        self.grpc = GrpcStructMapping()
        self.ice = IceStructMapping()
        self.java = JavaStructMapping()


class ProtoSource(Enum):
    """
    the source to generate .proto
    """
    ICE = 1
    JAVA = 2


class GrpcBaseField(object):
    """
    base field of proto struct, such as fields in enum, message, service
    """
    name: str
    struct: object
    # default 4 spaces indent
    line_indent = '    '

    def build(self, *mapping: GrpcFullMapping):
        pass


class GrpcBaseStruct(object):
    """
    base proto struct, such as enum, message, service

    :name: original name, such as Demo
    :leading_name: such as Grpc
    :optimized_name: leading_name with name, such as GrpcDemo
    """
    name: str
    leading_prefix: str
    source: ProtoSource
    source_type: str
    fields: List[GrpcBaseField]

    def __init__(self):
        self.leading_prefix = ''

    def get_optimized_name(self):
        return self.leading_prefix + self.name

    def build(self, *mapping: GrpcFullMapping):
        pass


class GrpcEnumField(GrpcBaseField):
    number: int
    comment: str

    def __init__(self, name, number):
        self.name = name
        self.number = number

    def build(self):
        res = self.line_indent
        return res + f'{self.name} = {self.number};\n'


class GrpcEnum(GrpcBaseStruct):
    comment: str
    fields: List[GrpcEnumField]

    def __init__(self, name, source_type):
        super().__init__()
        self.name = name
        self.source_type = source_type
        self.fields = []

    def add_field(self, field: GrpcEnumField):
        self.fields.append(field)

    def build(self, *mapping: GrpcFullMapping):
        res = f'enum {self.get_optimized_name()} ' + '{\n'
        for f in self.fields:
            res = res + f.build()
        return res + '}\n'


class GrpcField(GrpcBaseField):
    option: str
    f_type: str
    f_type_resolved: bool
    number: int
    comment: str = None

    def __init__(self, option, f_type, name, number):
        self.option = option
        self.f_type = f_type
        # default True when invoke this method
        self.f_type_resolved = True
        self.name = name
        self.number = number

    def set_f_type_resolved(self, f_type_resolved: bool):
        self.f_type_resolved = f_type_resolved

    def build(self, mapping: GrpcFullMapping):
        res = self.line_indent
        if self.option is not None:
            res = res + self.option + ' '
        if self.f_type in mapping.ice.sequence_dict:
            ice_f_type = mapping.ice.sequence_dict[self.f_type]
            self.f_type = 'repeated ' + mapping.ice.ice_2_proto_type_dict[ice_f_type]
        res = res + f'{self.f_type} {self.name} = {self.number};'
        if self.comment is not None and self.comment != '':
            res = f'{res} {self.comment}'
        return res + '\n'


class GrpcMessage(GrpcBaseStruct):
    comment: str
    fields: List[GrpcField]

    def __init__(self, name, source_type):
        super().__init__()
        self.name = name
        self.source_type = source_type
        self.fields = []

    def add_field(self, field: GrpcField):
        self.fields.append(field)

    def build(self, mapping: GrpcFullMapping):
        res = f'message {self.get_optimized_name()} ' + '{\n'
        for f in self.fields:
            res = res + f.build(mapping)
        return res + '}\n'


class GrpcMethod(GrpcBaseField):
    request: str
    response: str
    comment: str = None

    def __init__(self, name):
        self.name = name

    def build(self):
        res = ''
        if self.comment is not None and self.comment != '':
            res = self.line_indent + self.comment + '\n'
        return res + self.line_indent + f'rpc {self.name}({self.request}) returns({self.response});\n'


class GrpcService(GrpcBaseStruct):
    module_name: str
    comment: str
    fields: List[GrpcMethod]
    structs: List[GrpcBaseStruct]

    def __init__(self, name: str, grpc_config: GrpcConfig):
        """
        init GrpcService
        :param name: ice interface name
        """
        super().__init__()
        self.name = name
        self.fields = []
        self.structs = []
        self.module_name = self.get_optimized_name().replace(grpc_config.proto_config.service_suffix, '')

    def add_method(self, method: GrpcMethod):
        self.fields.append(method)

    def add_related_struct(self, struct_or_struct_name, mapping: GrpcFullMapping):
        struct = struct_or_struct_name

        if type(struct_or_struct_name) == str:
            struct_name = struct_or_struct_name.replace('repeated', '').strip()
            if 'map<' in struct_name and struct_name in mapping.grpc.map_dict:
                map_types = mapping.grpc.map_dict[struct_or_struct_name]
                if isinstance(map_types, GrpcMessage):
                    # add messages such as GrpcDepartmentHolder
                    self.add_related_struct(map_types.get_optimized_name(), mapping)
                else:
                    if map_types[0] in mapping.grpc.struct_dict:
                        self.add_related_struct(map_types[0], mapping)
                    if map_types[1] in mapping.grpc.struct_dict:
                        self.add_related_struct(map_types[1], mapping)
            elif struct_name in mapping.grpc.struct_dict:
                struct = mapping.grpc.struct_dict[struct_name]
        if isinstance(struct, GrpcBaseStruct) and struct.get_optimized_name() not in mapping.grpc.existed_struct_set:
            self.structs.append(struct)
            mapping.grpc.existed_struct_set.add(struct.get_optimized_name())
            if len(struct.fields) > 0:
                for item in struct.fields:
                    if isinstance(item, GrpcField):
                        self.add_related_struct(item.f_type, mapping)

    def enrich_related_struct(self, mapping: GrpcFullMapping):
        """
        pick up missed relating structs because of disorder in parse phase
        """
        if len(self.structs) > 0:
            for item in self.structs:
                if len(item.fields) > 0:
                    for f_item in item.fields:
                        if not isinstance(f_item, GrpcField):
                            continue
                        if not f_item.f_type_resolved and f_item.f_type in mapping.ice.ice_2_proto_type_dict:
                            grpc_type = mapping.ice.ice_2_proto_type_dict[f_item.f_type]
                            f_item.f_type = grpc_type
                            f_item.set_f_type_resolved(True)
                            self.add_related_struct(grpc_type, mapping)

    def build(self, mapping: GrpcFullMapping):
        self.enrich_related_struct(mapping)
        res = ''
        if len(self.structs) > 0:
            for item in self.structs:
                res += item.build(mapping)
                res += '\n'
        res = res + f'service {self.get_optimized_name()}' + ' {\n'
        for m in self.fields:
            res += m.build()
        return res + '}\n'

    def update_build(self, existed_message_set: set, existed_method_set: set,
                     lines: List, service_name_dict: dict, mapping: GrpcFullMapping):
        def filter_message(message: GrpcBaseStruct):
            return message.get_optimized_name() not in existed_message_set

        def filter_method(field):
            return field.name not in existed_method_set

        def grpc_service_exists():
            return self.get_optimized_name() in service_name_dict

        # check if service is in service mapping
        if self.name in mapping.filter_mapping.ice_2_grpc:
            self.name = mapping.filter_mapping.ice_2_grpc[self.name].grpc

        # enrich and filter existed
        self.enrich_related_struct(mapping)
        self.structs = [s for s in self.structs if filter_message(s)]
        self.fields = [f for f in self.fields if filter_method(f)]

        # determine where to add changed lines
        service_first_line_idx = -1
        service_last_line_idx = -1
        last_line_idx = len(lines) - 1
        if grpc_service_exists():
            service_first_line_idx, service_last_line_idx = service_name_dict[self.get_optimized_name()]

        # processing messages
        if len(self.structs) > 0:
            message_content = ''
            for item in self.structs:
                message_content += item.build(mapping)

            if not grpc_service_exists():
                # add another empty line before
                message_content = '\n' + message_content
                lines[last_line_idx] = lines[last_line_idx] + message_content
            else:
                # add another empty line after
                message_content += '\n'
                lines[service_first_line_idx] = message_content + lines[service_first_line_idx]

        # processing service methods
        if len(self.fields) > 0:
            # add leading empty line
            method_content = '\n'

            if not grpc_service_exists():
                method_content += f'service {self.get_optimized_name()}' + ' {\n'

            for item in self.fields:
                method_content += item.build()

            if not grpc_service_exists():
                method_content += '}\n'
                # just append to last line
                lines[last_line_idx] = lines[last_line_idx] + method_content
            else:
                # generally before '}'
                lines[service_last_line_idx] = method_content + lines[service_last_line_idx]
